﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using FastNet.Internal;
using ProtoBuf;

namespace FastNet
{
	/// <summary>
	/// The message used in the communication between the client and the server.
	/// </summary>
	public sealed class FastMessage : EventArgs
	{
		internal static FastMessage ReadFrom( Byte[] data )
		{
			Logic.Check<ArgumentNullException>( data != null, "data" );

			return new FastMessage( data );
		}


		/// <summary>
		/// Initializes a new message with the provided command.
		/// </summary>
		/// <param name="command">The command of the message.</param>
		public FastMessage( Int32 command )
			: this( command, false )
		{
		}


		/// <summary>
		/// Initializes a new message with the provided command and specifying if compression is enabled.
		/// </summary>
		/// <param name="command">The command of the message.</param>
		/// <param name="compress">Indicating whether to compress this message or not.</param>
		public FastMessage( Int32 command, Boolean compress )
		{
			this.Command	= command;
			this.Compress	= compress;
		}


		private FastMessage( Byte[] buffer )
		{
			Logic.Assert( buffer != null );

			MemoryStream	stream			= new MemoryStream( buffer, false );
			Stream			readableStream	= stream;

			this.Compress = stream.ReadByte() == 1;

			if ( this.Compress )
			{
				readableStream = new DeflateStream( stream, CompressionMode.Decompress );
			}

			using ( readableStream )
			using ( BinaryReader reader = new BinaryReader( readableStream, Encoding.UTF8 ) )
			{
				this.Command	= reader.ReadInt32();
				Byte count		= reader.ReadByte();

				Logic.Check<InvalidOperationException>( ( count >= 0 ) && ( count <= Byte.MaxValue ), "Invalid message. The size parameter is incorrect." );

				m_values = new Dictionary<String, Byte[]>( count );

				for ( Byte i = 0; i < count; ++i )
				{
					String key = reader.ReadString();
					Int32 length = reader.ReadInt32();
					Byte[] value = null;

					if ( length > -1 )
					{
						value = reader.ReadBytes( length );
					}

					m_values.Add( key, value );
				}
			}
		}


		/// <summary>
		/// Returns the command associated with the current message.
		/// </summary>
		public Int32 Command { get; private set; }


		/// <summary>
		/// Gets or sets whether to compress this message or not. Default is false.
		/// </summary>
		public Boolean Compress { get; private set; }


		/// <summary>
		/// Adds a new item in the message. Note that you can only add up to 255 items in a single message.
		/// </summary>
		/// <typeparam name="TItem">The type of the item being added. This type must be marked with [DataContract], [ProtoContract] or be a Serializable type.</typeparam>
		/// <param name="key">The key associated with the current item. Cannot be null.</param>
		/// <param name="value">The value. Can be null.</param>
		/// <returns>Returns self in order to be able to chain calls.</returns>
		public FastMessage Add<TItem>( String key, TItem value )
		{
			Logic.Check<ArgumentNullException>( key != null, "key" );

			if ( m_values.Count == Byte.MaxValue )
			{
				throw new InvalidOperationException( "FastMessage can only contain up to 255 values." );
			}

			if ( typeof( TItem ) == typeof( Byte[] ) )
			{
				m_values.Add( key, (Byte[])(Object)value );
			}
			else
			{
				using ( MemoryStream stream = new MemoryStream() )
				{
					Serializer.Serialize( stream, value );

					m_values.Add( key, stream.ToArray() );
				}
			}

			m_data = null;

			return this;
		}


		/// <summary>
		/// Returns an item from the message. Note that deserialization occurs on calling this method, and thus values returned from it should be cached for better performance.
		/// </summary>
		/// <typeparam name="TItem">The type of the requested item.</typeparam>
		/// <param name="key">The key associated with the item. Cannot be null.</param>
		public TItem Get<TItem>( String key )
		{
			Logic.Check<ArgumentNullException>( key != null, "key" );
			Logic.Check<KeyNotFoundException>( m_values.ContainsKey( key ), key );

			if ( typeof( TItem ) == typeof( Byte[] ) )
			{
				return (TItem)(Object)m_values[ key ];
			}
			else
			{
				using ( MemoryStream stream = new MemoryStream( m_values[ key ] ) )
				{
					return Serializer.Deserialize<TItem>( stream );
				}
			}
		}


		internal Byte[] GetData()
		{
			if ( m_data == null )
			{
				MemoryStream stream			= new MemoryStream();
				Stream		 writableStream	= stream;

				stream.WriteByte( this.Compress ? (Byte)1 : (Byte)0 );

				if ( this.Compress )
				{
					writableStream = new DeflateStream( stream, CompressionLevel.Fastest );
				}

				using ( writableStream )
				using ( BinaryWriter writer = new BinaryWriter( writableStream, Encoding.UTF8 ) )
				{
					writer.Write( this.Command );
					writer.Write( (Byte)m_values.Count );

					foreach ( var item in m_values )
					{
						writer.Write( item.Key );

						if ( item.Value != null )
						{
							writer.Write( item.Value.Length );
							writer.Write( item.Value );
						}
						else
						{
							writer.Write( -1 );
						}
					}

					writableStream.Close();

					m_data = stream.ToArray();
				}
			}

			return m_data;
		}


		private Dictionary<String, Byte[]>	m_values	= new Dictionary<String, Byte[]>( StringComparer.Ordinal );
		private Byte[]						m_data		= null;
	}
}